/*
 ------------------------------------------------------------------------------
 Copyright (C) 2006-2007 Team Blur.

 This file is part of the Quantum Engine source code.

 The Quantum Engine source code is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or (at your
 option) any later version.

 The Quantum Engine source code is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 more details.

 You should have received a copy of the GNU General Public License along with
 the Quantum Engine source code; if not, write to the Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 ------------------------------------------------------------------------------
*/


#include "r_local.h"


#define VIDEOS_HASH_SIZE				MAX_VIDEOS / 4


typedef struct {
	int				vr[256];
	int				ug[256];
	int				vg[256];
	int				ub[256];
} yuvTable_t;

static yuvTable_t	r_yuvTable;

static int			r_quadOffsets2[2][4];
static int			r_quadOffsets4[2][4];

static video_t *	r_videosHashTable[VIDEOS_HASH_SIZE];
static video_t *	r_videos[MAX_VIDEOS];
static int			r_numVideos;


/*
 ==================
 R_ApplyVector2x2
 ==================
*/
static void R_ApplyVector2x2 (video_t *video, int x, int y, const byte *indices){

	dword	*src, *dst;
	int		xp, yp;
	int		i;

	for (i = 0; i < 4; i++){
		xp = x + r_quadOffsets2[0][i];
		yp = y + r_quadOffsets2[1][i];

		src = (dword *)video->quadVectors + (indices[i] * 4);
		dst = (dword *)video->frameBuffer[0] + (yp * video->frameWidth + xp);

		dst[0] = src[0];
		dst[1] = src[1];

		dst += video->frameWidth;

		dst[0] = src[2];
		dst[1] = src[3];
	}
}

/*
 ==================
 R_ApplyVector4x4
 ==================
*/
static void R_ApplyVector4x4 (video_t *video, int x, int y, const byte *indices){

	dword	*src, *dst;
	int		xp, yp;
	int		i;

	for (i = 0; i < 4; i++){
		xp = x + r_quadOffsets4[0][i];
		yp = y + r_quadOffsets4[1][i];

		src = (dword *)video->quadVectors + (indices[i] * 4);
		dst = (dword *)video->frameBuffer[0] + (yp * video->frameWidth + xp);

		dst[0] = src[0];
		dst[1] = src[0];
		dst[2] = src[1];
		dst[3] = src[1];

		dst += video->frameWidth;

		dst[0] = src[0];
		dst[1] = src[0];
		dst[2] = src[1];
		dst[3] = src[1];

		dst += video->frameWidth;

		dst[0] = src[2];
		dst[1] = src[2];
		dst[2] = src[3];
		dst[3] = src[3];

		dst += video->frameWidth;

		dst[0] = src[2];
		dst[1] = src[2];
		dst[2] = src[3];
		dst[3] = src[3];
	}
}

/*
 ==================
 R_ApplyMotion4x4
 ==================
*/
static void R_ApplyMotion4x4 (video_t *video, int x, int y, int mx, int my, int mv){

	dword	*src, *dst;
	int		xp, yp;
	int		i;

	xp = x + 8 - (mv >> 4) - mx;
	yp = y + 8 - (mv & 15) - my;

	src = (dword *)video->frameBuffer[1] + (yp * video->frameWidth + xp);
	dst = (dword *)video->frameBuffer[0] + (y * video->frameWidth + x);

	for (i = 0; i < 4; i++, src += video->frameWidth, dst += video->frameWidth){
		dst[0] = src[0];
		dst[1] = src[1];
		dst[2] = src[2];
		dst[3] = src[3];
	}
}

/*
 ==================
 R_ApplyMotion8x8
 ==================
*/
static void R_ApplyMotion8x8 (video_t *video, int x, int y, int mx, int my, int mv){

	dword	*src, *dst;
	int		xp, yp;
	int		i;

	xp = x + 8 - (mv >> 4) - mx;
	yp = y + 8 - (mv & 15) - my;

	src = (dword *)video->frameBuffer[1] + (yp * video->frameWidth + xp);
	dst = (dword *)video->frameBuffer[0] + (y * video->frameWidth + x);

	for (i = 0; i < 8; i++, src += video->frameWidth, dst += video->frameWidth){
		dst[0] = src[0];
		dst[1] = src[1];
		dst[2] = src[2];
		dst[3] = src[3];
		dst[4] = src[4];
		dst[5] = src[5];
		dst[6] = src[6];
		dst[7] = src[7];
	}
}

/*
 ==================
 R_DecodeInfo
 ==================
*/
static void R_DecodeInfo (video_t *video, const byte *data){

	if (video->frameBuffer[0] && video->frameBuffer[1])
		return;		// Already allocated

	// Allocate the frame buffer
	video->frameWidth = data[0] | (data[1] << 8);
	video->frameHeight = data[2] | (data[3] << 8);

	if (!M_IsPowerOfTwo(video->frameWidth) || !M_IsPowerOfTwo(video->frameHeight))
		Com_Error(false, "R_UpdateVideo: size is not a power of two (%i x %i) (%s)", video->frameWidth, video->frameHeight, video->name);

	if (video->frameWidth > glConfig.maxTextureSize || video->frameHeight > glConfig.maxTextureSize)
		Com_Error(false, "R_UpdateVideo: size exceeds hardware limits (%i > %i or %i > %i) (%s)", video->frameWidth, glConfig.maxTextureSize, video->frameHeight, glConfig.maxTextureSize, video->name);

	video->frameBuffer[0] = (byte *)Mem_Alloc(video->frameWidth * video->frameHeight * 4, TAG_RENDERER);
	video->frameBuffer[1] = (byte *)Mem_Alloc(video->frameWidth * video->frameHeight * 4, TAG_RENDERER);
}

/*
 ==================
 R_DecodeCodeBook
 ==================
*/
static void R_DecodeCodeBook (video_t *video, const byte *data){

	int		numQuadVectors, numQuadCells;
	int		r, g, b;
	int		i;

	if (video->chunk.flags){
		numQuadVectors = (video->chunk.flags >> 8) & 0xFF;
		numQuadCells = (video->chunk.flags >> 0) & 0xFF;

		if (!numQuadVectors)
			numQuadVectors = 256;
	}
	else {
		numQuadVectors = 256;
		numQuadCells = 256;
	}

	// Decode YUV quad vectors to RGB
	for (i = 0; i < numQuadVectors; i++){
		r = r_yuvTable.vr[data[5]];
		g = r_yuvTable.ug[data[4]] + r_yuvTable.vg[data[5]];
		b = r_yuvTable.ub[data[4]];

		((byte *)&video->quadVectors[i].pixel[0])[0] = M_ClampByte(data[0] + r);
		((byte *)&video->quadVectors[i].pixel[0])[1] = M_ClampByte(data[0] - g);
		((byte *)&video->quadVectors[i].pixel[0])[2] = M_ClampByte(data[0] + b);
		((byte *)&video->quadVectors[i].pixel[0])[3] = 255;

		((byte *)&video->quadVectors[i].pixel[1])[0] = M_ClampByte(data[1] + r);
		((byte *)&video->quadVectors[i].pixel[1])[1] = M_ClampByte(data[1] - g);
		((byte *)&video->quadVectors[i].pixel[1])[2] = M_ClampByte(data[1] + b);
		((byte *)&video->quadVectors[i].pixel[1])[3] = 255;

		((byte *)&video->quadVectors[i].pixel[2])[0] = M_ClampByte(data[2] + r);
		((byte *)&video->quadVectors[i].pixel[2])[1] = M_ClampByte(data[2] - g);
		((byte *)&video->quadVectors[i].pixel[2])[2] = M_ClampByte(data[2] + b);
		((byte *)&video->quadVectors[i].pixel[2])[3] = 255;

		((byte *)&video->quadVectors[i].pixel[3])[0] = M_ClampByte(data[3] + r);
		((byte *)&video->quadVectors[i].pixel[3])[1] = M_ClampByte(data[3] - g);
		((byte *)&video->quadVectors[i].pixel[3])[2] = M_ClampByte(data[3] + b);
		((byte *)&video->quadVectors[i].pixel[3])[3] = 255;

		data += 6;
	}

	// Copy quad cells
	for (i = 0; i < numQuadCells; i++){
		video->quadCells[i].index[0] = data[0];
		video->quadCells[i].index[1] = data[1];
		video->quadCells[i].index[2] = data[2];
		video->quadCells[i].index[3] = data[3];

		data += 4;
	}
}

/*
 ==================
 R_DecodeVideo
 ==================
*/
static void R_DecodeVideo (video_t *video, const byte *data){

	byte	*buffer;
	int		vqFlag, vqFlagPos, vqCode;
	int		xPos, yPos, xMot, yMot;
	int		x, y, xp, yp;
	int		index;
	int		i;

	if (!video->frameBuffer[0] || !video->frameBuffer[1])
		return;		// No frame buffer

	vqFlag = 0;
	vqFlagPos = 0;

	xPos = 0;
	yPos = 0;

	xMot = (char)((video->chunk.flags >> 8) & 0xFF);
	yMot = (char)((video->chunk.flags >> 0) & 0xFF);

	index = 0;

	while (1){
		for (y = yPos; y < yPos + 16; y += 8){
			for (x = xPos; x < xPos + 16; x += 8){
				if (!vqFlagPos){
					vqFlagPos = 7;
					vqFlag = data[index+0] | (data[index+1] << 8);

					index += 2;
				}
				else
					vqFlagPos--;

				vqCode = vqFlag & 0xC000;
				vqFlag <<= 2;
				
				switch (vqCode){
				case 0x4000:
					R_ApplyMotion8x8(video, x, y, xMot, yMot, data[index]);

					index += 1;

					break;
				case 0x8000:
					R_ApplyVector4x4(video, x, y, video->quadCells[data[index]].index);

					index += 1;

					break;
				case 0xC000:
					for (i = 0; i < 4; i++){
						xp = x + r_quadOffsets4[0][i];
						yp = y + r_quadOffsets4[1][i];

						if (!vqFlagPos){
							vqFlagPos = 7;
							vqFlag = data[index+0] | (data[index+1] << 8);

							index += 2;
						}
						else
							vqFlagPos--;

						vqCode = vqFlag & 0xC000;
						vqFlag <<= 2;

						switch (vqCode){
						case 0x4000:
							R_ApplyMotion4x4(video, xp, yp, xMot, yMot, data[index]);

							index += 1;

							break;
						case 0x8000:
							R_ApplyVector2x2(video, xp, yp, video->quadCells[data[index]].index);

							index += 1;

							break;
						case 0xC000:
							R_ApplyVector2x2(video, xp, yp, &data[index]);

							index += 4;

							break;
						}
					}

					break;
				}
			}
		}

		xPos += 16;
		if (xPos >= video->frameWidth){
			xPos -= video->frameWidth;

			yPos += 16;
			if (yPos >= video->frameHeight)
				break;
		}
	}

	// Copy or swap the buffers
	if (video->currentFrame == 0)
		memcpy(video->frameBuffer[1], video->frameBuffer[0], video->frameWidth * video->frameHeight * 4);
	else {
		buffer = video->frameBuffer[0];
		video->frameBuffer[0] = video->frameBuffer[1];
		video->frameBuffer[1] = buffer;
	}

	video->currentFrame++;
}

/*
 ==================
 R_DecodeChunk
 ==================
*/
static bool R_DecodeChunk (video_t *video){

	if (video->offset >= video->size)
		return false;	// Finished

	// Parse the chunk header
	video->chunk.id = video->header[0] | (video->header[1] << 8);
	video->chunk.size = video->header[2] | (video->header[3] << 8) | (video->header[4] << 16) | (video->header[5] << 24);
	video->chunk.flags = video->header[6] | (video->header[7] << 8);

	if (video->chunk.id == ROQ_IDENT || video->chunk.size > ROQ_MAX_CHUNK_SIZE)
		return false;	// Invalid chunk

	// Read the chunk data and the next chunk header
	FS_Read(video->data, video->chunk.size + ROQ_CHUNK_HEADER_SIZE, video->file);
	video->offset += video->chunk.size + ROQ_CHUNK_HEADER_SIZE;

	video->header = video->data + video->chunk.size;

	// Decode the chunk data
	switch (video->chunk.id){
	case ROQ_QUAD_INFO:
		R_DecodeInfo(video, video->data);
		break;
	case ROQ_QUAD_CODEBOOK:
		R_DecodeCodeBook(video, video->data);
		break;
	case ROQ_QUAD_VQ:
		R_DecodeVideo(video, video->data);
		break;
	}

	return true;
}

/*
 ==================
 R_UpdateVideoTexture
 ==================
*/
static void R_UpdateVideoTexture (video_t *video){

	if (r_skipDynamicTextures->integerValue)
		return;

	tr.pc.dynamicTextures++;
	tr.pc.dynamicTexturePixels += video->frameWidth * video->frameHeight;

	// Update the texture
	if (video->frameWidth == tr.cinematicTexture->width && video->frameHeight == tr.cinematicTexture->height){
		GL_BindTexture(tr.cinematicTexture);

		qglTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, video->frameWidth, video->frameHeight, GL_RGBA, GL_UNSIGNED_BYTE, video->frameBuffer[1]);
	}
	else {
		// Reallocate the texture
		tr.cinematicTexture->width = video->frameWidth;
		tr.cinematicTexture->height = video->frameHeight;

		if (tr.cinematicTexture->samples >= 3)
			tr.cinematicTexture->size = video->frameWidth * video->frameHeight * 4;
		else
			tr.cinematicTexture->size = video->frameWidth * video->frameHeight * tr.cinematicTexture->samples;

		tr.cinematicTexture->frameCount = 0;

		GL_BindTexture(tr.cinematicTexture);

		qglTexImage2D(GL_TEXTURE_2D, 0, tr.cinematicTexture->format, video->frameWidth, video->frameHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, video->frameBuffer[1]);
	}
}

/*
 ==================
 R_PlayVideo
 ==================
*/
video_t *R_PlayVideo (const char *name){

	video_t			*video;
	roqChunk_t		chunk;
	fileHandle_t	file;
	int				size;
	byte			header[ROQ_CHUNK_HEADER_SIZE];
	uint			hashKey;

	// See if already playing
	hashKey = Str_HashKey(name, VIDEOS_HASH_SIZE, false);

	for (video = r_videosHashTable[hashKey]; video; video = video->nextHash){
		if (!Str_ICompare(video->name, name))
			return video;
	}

	// Open the file
	size = FS_OpenFile(name, &file, FS_READ);
	if (!file)
		return NULL;

	// Parse the header
	FS_Read(header, sizeof(header), file);

	chunk.id = header[0] | (header[1] << 8);
	chunk.size = header[2] | (header[3] << 8) | (header[4] << 16) | (header[5] << 24);
	chunk.flags = header[6] | (header[7] << 8);

	if (chunk.id != ROQ_IDENT){
		FS_CloseFile(file);
		Com_Error(false, "R_PlayVideo: invalid RoQ header (%s)", name);
	}

	// Play the video
	if (r_numVideos == MAX_VIDEOS){
		FS_CloseFile(file);
		Com_Error(false, "R_PlayVideo: MAX_VIDEOS hit");
	}

	r_videos[r_numVideos++] = video = (video_t *)Mem_Alloc(sizeof(video_t), TAG_RENDERER);

	// Fill it in
	Str_Copy(video->name, name, sizeof(video->name));

	video->file = file;
	video->size = size;
	video->offset = sizeof(header);

	video->frameWidth = 0;
	video->frameHeight = 0;
	video->frameRate = (chunk.flags != 0) ? chunk.flags : 30;
	video->frameBuffer[0] = NULL;
	video->frameBuffer[1] = NULL;

	video->startTime = 0;
	video->currentFrame = 0;

	// Read the first chunk header
	FS_Read(video->data, ROQ_CHUNK_HEADER_SIZE, video->file);
	video->offset += ROQ_CHUNK_HEADER_SIZE;

	video->header = video->data;

	// Add to hash table
	video->nextHash = r_videosHashTable[hashKey];
	r_videosHashTable[hashKey] = video;

	return video;
}

/*
 ==================
 R_UpdateVideo
 ==================
*/
void R_UpdateVideo (video_t *video, int time){

	int		frame;

	if (r_skipVideos->integerValue)
		return;

	// If we don't have a frame yet, set the start time
	if (!video->currentFrame)
		video->startTime = time;

	// Check if a new frame is needed
	frame = (time - video->startTime) * video->frameRate / 1000;
	if (frame < 1)
		frame = 1;

	// Never drop too many frames in a row
	if (frame > video->currentFrame + 3){
		frame = video->currentFrame + 3;

		video->startTime = time - frame * 1000 / video->frameRate;
	}

	// Decode chunks until the desired frame is reached
	while (frame > video->currentFrame){
		if (R_DecodeChunk(video))
			continue;

		// Make sure we don't get stuck into an infinite loop
		if (video->currentFrame == 0)
			break;

		// Restart the video
		FS_Seek(video->file, 0, FS_SEEK_SET);

		video->startTime = time;
		video->currentFrame = 0;

		// Read the first chunk header
		FS_Read(video->data, ROQ_CHUNK_HEADER_SIZE * 2, video->file);
		video->offset = ROQ_CHUNK_HEADER_SIZE * 2;

		video->header = video->data + ROQ_CHUNK_HEADER_SIZE;

		// We will run the first frame now
		frame = 1;
	}

	// Update the video texture
	R_UpdateVideoTexture(video);
}

/*
 ==================
 R_ListVideos_f
 ==================
*/
static void R_ListVideos_f (void){

	video_t	*video;
	int		bytes = 0;
	int		i;

	Com_Printf("\n");
	Com_Printf("      -w-- -h-- -size- fps -name--------\n");

	for (i = 0; i < r_numVideos; i++){
		video = r_videos[i];

		bytes += video->frameWidth * video->frameHeight * 4 * 2;

		Com_Printf("%4i: ", i);

		Com_Printf("%4i %4i ", video->frameWidth, video->frameHeight);

		Com_Printf("%5ik ", (video->frameWidth * video->frameHeight * 4 * 2) >> 10);

		Com_Printf("%3i ", video->frameRate);

		Com_Printf("%s\n", video->name);
	}

	Com_Printf("----------------------------------------\n");
	Com_Printf("%i total videos\n", r_numVideos);
	Com_Printf("%.2f MB of video data\n", bytes * (1.0f/1048576));
	Com_Printf("\n");
}

/*
 ==================
 R_InitVideos
 ==================
*/
void R_InitVideos (void){

	float	f;
	int		i;

	// Register our commands
	Cmd_AddCommand("listVideos", R_ListVideos_f, "Lists loaded videos");

	// Build YUV table
	for (i = 0; i < 256; i++){
		f = (float)(i - 128);

		r_yuvTable.vr[i] = M_Ftol(f * 1.40200f);
		r_yuvTable.ug[i] = M_Ftol(f * 0.34414f);
		r_yuvTable.vg[i] = M_Ftol(f * 0.71414f);
		r_yuvTable.ub[i] = M_Ftol(f * 1.77200f);
	}

	// Set up quad offsets
	for (i = 0; i < 4; i++){
		r_quadOffsets2[0][i] = 2 * (i & 1);
		r_quadOffsets2[1][i] = 2 * (i >> 1);

		r_quadOffsets4[0][i] = 4 * (i & 1);
		r_quadOffsets4[1][i] = 4 * (i >> 1);
	}
}

/*
 ==================
 R_ShutdownVideos
 ==================
*/
void R_ShutdownVideos (void){

	video_t	*video;
	int		i;

	// Unregister our commands
	Cmd_RemoveCommand("listVideos");

	// Close all videos
	for (i = 0; i < r_numVideos; i++){
		video = r_videos[i];

		if (video->file)
			FS_CloseFile(video->file);
	}

	// Clear video list
	memset(r_videosHashTable, 0, sizeof(r_videosHashTable));
	memset(r_videos, 0, sizeof(r_videos));

	r_numVideos = 0;
}
